﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Linq;
using System.Diagnostics;
using System.Windows.Media;
using System.ComponentModel;
using System.Windows.Shapes;
using System.Windows.Input;
using System.Windows.Controls.DataVisualization.Charting;

namespace DemoZoomChart
{
    public enum ChartToolMode
    {
        ZoomBox,
        Pan
    }

    /// <summary>
    /// Charting sample that demonstrates zooming.
    /// </summary>
    public partial class SeriesZoomExtended : UserControl
    {
        private Panel chartPlotArea;
        private ScrollViewer chartScrollArea;
        private Border zoomBox;
        private Canvas zoomCanvas;

        private Panel xPlotArea;
        private ScrollViewer xScrollArea;
        private Panel yPlotArea;
        private ScrollViewer yScrollArea;
        private Point mouseStart;
        private ChartToolMode mode = ChartToolMode.Pan;

        private bool zoomBoxDrawing = false;
        private bool panning = false;
        private double zoomFactor = 1.0;
        private double maxZoomFactor = 10;
        private DateTime xMin;
        private DateTime xMax;

        private List<SpanOption> spanOptions = SpanOption.GetSpanOptions();

        public List<SpanOption> SpanOptions
        {
            get { return this.spanOptions; }
        }

        public int SelectedSpan { get; set; }

        #region Template Control Access

        private Panel ChartPlotArea
        {
            get
            {
                if (chartPlotArea == null)
                {
                    chartPlotArea = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ChartArea")).FirstOrDefault() as Panel;
                }

                return chartPlotArea;
            }
        }

        private ScrollViewer ChartScrollArea
        {
            get
            {
                if (chartScrollArea == null)
                {
                    chartScrollArea = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ChartScrollArea")).FirstOrDefault() as ScrollViewer;
                }
                return chartScrollArea;
            }
        }

        private Border ZoomBox
        {
            get
            {
                if (zoomBox == null)
                {
                    zoomBox = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ZoomBox")).FirstOrDefault() as Border;
                }
                return zoomBox;
            }
        }

        private Canvas ZoomCanvas
        {
            get
            {
                if (zoomBox == null)
                {
                    zoomCanvas = GetLogicalChildrenBreadthFirst(ZoomChart).Where(element => element.Name.Equals("ZoomCanvas")).FirstOrDefault() as Canvas;
                }
                return zoomCanvas;
            }
        }

        private Panel XPlotArea
        {
            get
            {
                if (xPlotArea == null)
                {
                    xPlotArea = GetLogicalChildrenBreadthFirst(ZoomXAxis).Where(element => element.Name.Equals("ChartArea")).FirstOrDefault() as Panel;
                }

                return xPlotArea;
            }
        }

        private ScrollViewer XScrollArea
        {
            get
            {
                if (xScrollArea == null)
                {
                    xScrollArea = GetLogicalChildrenBreadthFirst(ZoomXAxis).Where(element => element.Name.Equals("ChartScrollArea")).FirstOrDefault() as ScrollViewer;
                }
                return xScrollArea;
            }
        }

        private Panel YPlotArea
        {
            get
            {
                if (yPlotArea == null)
                {
                    yPlotArea = GetLogicalChildrenBreadthFirst(ZoomYAxis).Where(element => element.Name.Equals("ChartArea")).FirstOrDefault() as Panel;
                }

                return yPlotArea;
            }
        }

        private ScrollViewer YScrollArea
        {
            get
            {
                if (yScrollArea == null)
                {
                    yScrollArea = GetLogicalChildrenBreadthFirst(ZoomYAxis).Where(element => element.Name.Equals("ChartScrollArea")).FirstOrDefault() as ScrollViewer;
                }
                return yScrollArea;
            }
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the ZoomIntoChartSample class.
        /// </summary>
        public SeriesZoomExtended()
        {
            this.DataContext = this;
            InitializeComponent();

            this.Loaded += ZoomIntoChartSample_Loaded;
        }

        /// <summary>
        /// Force an update of the chart.
        /// </summary>
        /// <param name="sender">The ZoomIntoChartSample instance.</param>
        /// <param name="e">Event arguments.</param>
        private void ZoomIntoChartSample_Loaded(object sender, RoutedEventArgs e)
        {
            // Bind data
            GizmoPopularityPollCollection gizmos = new GizmoPopularityPollCollection();
            WidgetPopularityPollCollection widgets = new WidgetPopularityPollCollection();

            this.BindData(gizmos, widgets);

            this.ZoomChart.Focus();
        }

        // Manually attach mouse events
        private void ChartScrollArea_Loaded(object sender, RoutedEventArgs e)
        {
            this.cboSpan.SelectedIndex = 0;
            this.ChartPlotArea.MouseLeftButtonDown += this.ChartScrollArea_MouseLeftButtonDown;
            this.ChartPlotArea.MouseWheel += this.ChartScrollArea_MouseWheel;
        }

        // X Updated
        private void XScrollArea_LayoutUpdated(object sender, EventArgs e)
        {
            this.ChartScrollArea.ScrollToHorizontalOffset(this.XScrollArea.HorizontalOffset);
            this.ChartPlotArea.Width = this.XPlotArea.Width;
            this.ChartPlotArea.UpdateLayout();
            this.ChartScrollArea.UpdateLayout();
        }

        // Y Updated
        private void YScrollArea_LayoutUpdated(object sender, EventArgs e)
        {
            this.ChartScrollArea.ScrollToVerticalOffset(this.YScrollArea.VerticalOffset);
            this.ChartPlotArea.Height = this.YPlotArea.Height;
            this.ChartPlotArea.UpdateLayout();
            this.ChartScrollArea.UpdateLayout();
        }

        #region Chart Setup Methods

        public void BindData(GizmoPopularityPollCollection gizmos, WidgetPopularityPollCollection widgets)
        {
            // Bind series'
            LineSeries series1 = new LineSeries();
            series1.ItemsSource = gizmos;
            series1.Title = "Gizmo Popularity";
            series1.IndependentValueBinding = new System.Windows.Data.Binding("Date");
            series1.DependentValueBinding = new System.Windows.Data.Binding("Percent");

            LineSeries series2 = new LineSeries();
            series2.ItemsSource = widgets;
            series2.Title = "Widget Popularity";
            series2.IndependentValueBinding = new System.Windows.Data.Binding("Date");
            series2.DependentValueBinding = new System.Windows.Data.Binding("Percent");

            this.ZoomChart.Series.Add(series1);
            this.ZoomChart.Series.Add(series2);

            this.ZoomChart.UpdateLayout();

            // Get Axis limits            
            DateTime minDate1 = gizmos.Min(d => d.Date);
            DateTime maxDate1 = gizmos.Max(d => d.Date);
            DateTime minDate2 = widgets.Min(d => d.Date);
            DateTime maxDate2 = widgets.Max(d => d.Date);

            this.xMin = minDate1 < minDate2 ? minDate1 : minDate2;
            this.xMax = maxDate1 > maxDate2 ? maxDate1 : maxDate2;

            // Size X
            (this.ZoomXAxis.Axes[0] as DateTimeAxis).Minimum = (this.ZoomChart.Axes[0] as DateTimeAxis).ActualMinimum;
            (this.ZoomXAxis.Axes[0] as DateTimeAxis).Maximum = (this.ZoomChart.Axes[0] as DateTimeAxis).ActualMaximum;

            // Size Y
            (this.ZoomYAxis.Axes[0] as LinearAxis).Minimum = (this.ZoomChart.Axes[1] as LinearAxis).ActualMinimum;
            (this.ZoomYAxis.Axes[0] as LinearAxis).Maximum = (this.ZoomChart.Axes[1] as LinearAxis).ActualMaximum;

            // Update Y Column
            this.UpdateYColumn();
        }

        private void UpdateYColumn()
        {
            this.ZoomYAxis.UpdateLayout();

            double width = 0.0d; // 25.0d; // Scroller width

            width += (this.ZoomYAxis.Axes[0] as LinearAxis).ActualWidth;

            this.yColumn.Width = new GridLength(width);

        }

        #endregion

        #region Template helper methods

        private IEnumerable<FrameworkElement> GetLogicalChildrenBreadthFirst(FrameworkElement parent)
        {
            Debug.Assert(parent != null, "The parent cannot be null.");

            Queue<FrameworkElement> queue =
                new Queue<FrameworkElement>(GetVisualChildren(parent).OfType<FrameworkElement>());

            while (queue.Count > 0)
            {
                FrameworkElement element = queue.Dequeue();
                yield return element;

                foreach (FrameworkElement visualChild in GetVisualChildren(element).OfType<FrameworkElement>())
                {
                    queue.Enqueue(visualChild);
                }
            }
        }

        private IEnumerable<DependencyObject> GetVisualChildren(DependencyObject parent)
        {
            Debug.Assert(parent != null, "The parent cannot be null.");

            int childCount = VisualTreeHelper.GetChildrenCount(parent);
            for (int counter = 0; counter < childCount; counter++)
            {
                yield return VisualTreeHelper.GetChild(parent, counter);
            }
        }

        #endregion

        #region Mouse Events

        private void ChartScrollArea_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            this.mouseStart = e.GetPosition(this.ChartPlotArea);

            if (mode == ChartToolMode.Pan)
            {
                this.Cursor = Cursors.Hand;
                this.panning = true;
            }

            if (mode == ChartToolMode.ZoomBox)
            {
                this.Cursor = Cursors.Stylus;
                zoomBoxDrawing = true;

                this.StartZoomBox(e.GetPosition(this.ZoomCanvas));
            }
        }

        private void ChartScrollArea_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (mode == ChartToolMode.Pan && panning)
            {
                Point p = e.GetPosition(this.ChartPlotArea);
                this.Pan(p.X - this.mouseStart.X, p.Y - this.mouseStart.Y);
            }
            else if (mode == ChartToolMode.ZoomBox && zoomBoxDrawing)
            {
                this.MoveZoomBox(e.GetPosition(this.ZoomCanvas));
            }
        }

        private void ChartScrollArea_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (mode == ChartToolMode.ZoomBox && zoomBoxDrawing)
            {
                this.EndZoomBox(e.GetPosition(this.ChartPlotArea));
            }

            this.zoomBoxDrawing = false;
            this.panning = false;
            this.Cursor = Cursors.Arrow;
        }

        private void ChartScrollArea_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
            this.ZoomDelta(e.Delta, e.GetPosition(this.ChartPlotArea));
        }

        #endregion

        #region Toolbar Events

        private void btnPan_Click(object sender, RoutedEventArgs e)
        {
            this.Cursor = Cursors.Hand;
            this.mode = ChartToolMode.Pan;
        }

        private void btnZoom_Click(object sender, RoutedEventArgs e)
        {
            this.Cursor = Cursors.Stylus;
            this.mode = ChartToolMode.ZoomBox;
        }

        private void btnZoomCancel_Click(object sender, RoutedEventArgs e)
        {
            this.ResetZoom();
        }

        private void cboSpan_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (cboSpan != null && this.cboSpan.SelectedIndex > 0)
            {
                // Time spans
                TimeSpan? tsSpan = (this.cboSpan.SelectedItem as SpanOption).TS.Value;

                if(tsSpan.HasValue)
                    this.ChangeSpan(tsSpan.Value);
            }
            else
            {
                this.ResetZoom();
            }
        }

        #endregion

        #region Zoom methods

        private double ZoomToPoint_Zoom;
        private Point ZoomToPoint_Point;
        private bool ZoomToPoint_CallBackAdded = false;
        private bool ZoomToPoint_CallExpected = false;

        private void ZoomToPoint(double zoom, Point p)
        {
            this.ZoomToPoint_Zoom = zoom;
            this.ZoomToPoint_Point = p;
            this.ZoomToPoint_CallExpected = true;

            // Prevent callback being added multiple times
            if (!ZoomToPoint_CallBackAdded)
            {
                this.ZoomToPoint_CallBackAdded = true;

                // Fade out
                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ZoomToPoint_CallExpected)
                        {
                            this.ZoomToPoint_CallExpected = false;

                            this.zoomFactor *= this.ZoomToPoint_Zoom;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = XPlotArea.ActualWidth * this.ZoomToPoint_Zoom;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = YPlotArea.ActualHeight * this.ZoomToPoint_Zoom;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // X Pan
                            double xPan = this.ZoomToPoint_Point.X * this.ZoomToPoint_Zoom - (XScrollArea.ViewportWidth / 2);
                            this.XScrollArea.ScrollToHorizontalOffset(xPan);

                            // Y Pan
                            double yPan = this.ZoomToPoint_Point.Y * this.ZoomToPoint_Zoom - (YScrollArea.ViewportHeight / 2);
                            this.YScrollArea.ScrollToVerticalOffset(yPan);

                            // Update Y Column
                            this.UpdateYColumn();

                            // Fade In
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin();
        }

        // Callback variables
        private double ZoomDelta_Zoom;
        private Point ZoomDelta_Point;
        private bool ZoomDelta_CallBackAdded = false;
        private bool ZoomDelta_CallExpected = false;

        private void ZoomDelta(double delta, Point p)
        {
            double zoom = 1;

            if (delta > 0 && zoomFactor * 1.2 <= maxZoomFactor)
            {
                zoom = 1.2;
            }
            else if (delta < 0 && zoomFactor * 0.8 >= 1)
            {
                zoom = 0.8;
            }
            else if (delta < 0)
            {
                this.ResetZoom();
                return;
            }

            // Callback variables
            this.ZoomDelta_Zoom = zoom;
            this.ZoomDelta_Point = p;
            this.ZoomDelta_CallExpected = true;

            // Prevent callback being added multiple times
            if (!ZoomDelta_CallBackAdded)
            {
                this.ZoomDelta_CallBackAdded = true;

                // Fade out
                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ZoomDelta_CallExpected)
                        {
                            this.ZoomDelta_CallExpected = false;
                            this.zoomFactor *= ZoomDelta_Zoom;

                            // X Margin from scroll edge
                            double xMargin = ZoomDelta_Point.X - this.XScrollArea.HorizontalOffset;

                            // Y Margin from scroll edge
                            double yMArgin = ZoomDelta_Point.Y - this.YScrollArea.VerticalOffset;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = ZoomDelta_Zoom * this.XPlotArea.ActualWidth;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = ZoomDelta_Zoom * YPlotArea.ActualHeight;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // X Pan
                            double xPan = (ZoomDelta_Point.X * ZoomDelta_Zoom) - xMargin;
                            this.XScrollArea.ScrollToHorizontalOffset(xPan);

                            // Y Pan
                            double yPan = (ZoomDelta_Point.Y * ZoomDelta_Zoom) - yMArgin;
                            this.YScrollArea.ScrollToVerticalOffset(yPan);

                            // Fade In
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin();
        }


        private void StartZoomBox(Point zoomPoint)
        {
            // Draw ZoomToPoint_Zoom box at point
            ZoomBox.Height = 0.0;
            ZoomBox.Width = 0.0;
            ZoomBox.SetValue(Canvas.TopProperty, zoomPoint.Y);
            ZoomBox.SetValue(Canvas.LeftProperty, zoomPoint.X);
            ZoomBox.Visibility = System.Windows.Visibility.Visible;
        }

        private void MoveZoomBox(Point zoomPoint)
        {
            double left = (double)ZoomBox.GetValue(Canvas.LeftProperty);
            double top = (double)ZoomBox.GetValue(Canvas.TopProperty);

            // Resize Repostition if required
            if (zoomPoint.X > left)
            {
                ZoomBox.Width = zoomPoint.X - left;
            }
            else
            {
                ZoomBox.Width += left - zoomPoint.X;
                ZoomBox.SetValue(Canvas.LeftProperty, zoomPoint.X);
            }

            if (zoomPoint.Y > top)
            {
                ZoomBox.Height = zoomPoint.Y - top;
            }
            else
            {
                ZoomBox.Height += top - zoomPoint.Y;
                ZoomBox.SetValue(Canvas.TopProperty, zoomPoint.Y);
            }
        }

        private void EndZoomBox(Point zoomPoint)
        {
            // Zoom box top and left relative to chart
            double left = mouseStart.X < zoomPoint.X ? mouseStart.X : zoomPoint.X;
            double top = mouseStart.Y < zoomPoint.Y ? mouseStart.Y : zoomPoint.Y;

            // Zoom box centres
            double centreX = left + (this.ZoomBox.Width / 2);
            double centreY = top + (this.ZoomBox.Height / 2);

            // Chart area must be zoomed to allow ZoomToPoint_Zoom box to fill scroll viewer
            double xFactor = this.ChartScrollArea.ViewportWidth / this.ZoomBox.Width;
            double yFactor = this.ChartScrollArea.ViewportHeight / this.ZoomBox.Height;

            // Zoom to smallest factor
            double zoom = xFactor < yFactor ? xFactor : yFactor;

            // Check ZoomToPoint_Zoom bounds
            if (zoom * this.zoomFactor >= 1 && zoom * this.zoomFactor < this.maxZoomFactor)
            {
                // Centres
                Point p = new Point(centreX, centreY);

                // Zoom
                this.ZoomToPoint(zoom, p);
            }

            this.ZoomBox.Visibility = System.Windows.Visibility.Collapsed;
        }

        // Callback variables
        private double ChangeSpan_Zoom;
        private bool ChangeSpan_CallBackAdded = false;
        private bool ChangeSpan_CallExpected = false;

        // Change chart span
        private void ChangeSpan(TimeSpan tsSpan)
        {
            // Time spans
            TimeSpan tsX = xMax - xMin;

            // Zoom factor
            double zoom = (tsX.Ticks / tsSpan.Ticks);
            if (zoom == 0) zoom = 1.0d;

            // Callback variables
            this.ChangeSpan_Zoom = zoom;
            this.ChangeSpan_CallExpected = true;

            // Prevent callback being added multiple times
            if (!ChangeSpan_CallBackAdded)
            {
                this.ChangeSpan_CallBackAdded = true;

                // Fade out
                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ChangeSpan_CallExpected)
                        {
                            this.ChangeSpan_CallExpected = false;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = ChartScrollArea.ViewportWidth * ChangeSpan_Zoom;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = ChartScrollArea.ViewportHeight * 1;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // Update Y Column
                            this.UpdateYColumn();

                            // Fade In
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin();
        }

        // Callback variables
        private bool ResetZoom_CallbackAdded = false;
        private bool ResetZoom_CallbackExpected = false;

        private void ResetZoom()
        {
            this.ResetZoom_CallbackExpected = true;
            this.zoomFactor = 1.0;

            // Fade chart out
            if (!ResetZoom_CallbackAdded)
            {
                ResetZoom_CallbackAdded = true;

                this.fadeChartOut.Completed += new EventHandler(delegate(object o, EventArgs e)
                    {
                        // Check this is the right event handler
                        if (this.ResetZoom_CallbackExpected)
                        {
                            this.ResetZoom_CallbackExpected = false;

                            // X Width ZoomToPoint_Zoom
                            this.XPlotArea.Width = XScrollArea.ViewportWidth;
                            this.XPlotArea.UpdateLayout();
                            this.XScrollArea.UpdateLayout();

                            // Y Height ZoomToPoint_Zoom
                            this.YPlotArea.Height = YScrollArea.ViewportHeight;
                            this.YPlotArea.UpdateLayout();
                            this.YScrollArea.UpdateLayout();

                            // Fade chart in
                            this.fadeChartIn.Begin();

                            return;
                        }
                    });
            }

            this.fadeChartOut.Begin();
        }

        #endregion

        #region Pan

        private void Pan(double x, double y)
        {
            // X Pan
            this.XScrollArea.ScrollToHorizontalOffset(ChartScrollArea.HorizontalOffset - x);

            // Y Pan
            this.YScrollArea.ScrollToVerticalOffset(ChartScrollArea.VerticalOffset - y);
        }

        #endregion
    }

    public class SpanOption
    {
        public string Name { get; private set; }
        public TimeSpan? TS { get; private set; }

        public SpanOption(string name, TimeSpan? ts)
        {
            this.Name = name;
            this.TS = ts;
        }

        public static List<SpanOption> GetSpanOptions()
        {
            return new List<SpanOption>()
            {
                new SpanOption("No Span", null),
                new SpanOption("5 Days", new TimeSpan(5, 0, 0, 0)),
                new SpanOption("10 Days", new TimeSpan(10, 0, 0, 0)),
                new SpanOption("15 Days", new TimeSpan(15, 0, 0, 0)),
                new SpanOption("30 Days", new TimeSpan(30, 0, 0, 0))
            };
        }
    }
}